package com.shutong.edu.invoke.retry.processor;

import com.shutong.edu.invoke.retry.annotation.EnableRetry;
import com.shutong.edu.invoke.retry.common.RetryCheck;
import com.shutong.edu.invoke.retry.exception.RetryException;
import com.shutong.edu.invoke.retry.model.RetryInfo;
import com.shutong.edu.invoke.retry.provider.FallbackProvider;
import com.shutong.edu.invoke.retry.util.FallbackUtil;
import org.apache.commons.lang3.StringUtils;
import org.mvel2.MVEL;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;

/**
 *
 * @ClassName: AbstractRetryProcessor.java
 * @author: Naughty Guo
 * @date: Jun 2, 2016
 */
public abstract class AbstractRetryProcessor {

    private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRetryProcessor.class);

    /**
     * invoke the target method.
     *
     * @param retryInfo
     * @return
     * @throws Throwable
     * @author Naughty Guo Jun 2, 2016
     */
    protected abstract Object doInvoke(RetryInfo retryInfo) throws Throwable;

    /**
     * retry process.
     *
     * @param retryInfo
     * @return Object
     * @author Naughty Guo Jun 2, 2016
     */
    public Object retryProcess(RetryInfo retryInfo) {
        if (doRetry(retryInfo)) {
            fillRetryInfo(retryInfo);
            return invokeRetryProcess(retryInfo);
        }
        return invoke(retryInfo);
    }

    /**
     * invoke the target method.
     *
     * @param retryInfo
     * @return
     * @author Naughty Guo Jun 2, 2016
     */
    private Object invoke(RetryInfo retryInfo) {
        try {
            return doInvoke(retryInfo);
        } catch (Throwable e) {
            if (!retryInfo.isEnableRetry()) {
                LOGGER.error("Invoke method error, method is {}", retryInfo.getTargetMethodName(), e);
                throw new RuntimeException("Invoke method error.", e);
            } else {
                retryInfo.setInvokeException(e);
                throw new RetryException("Invoke method error.", e);
            }
        }
    }

    /**
     * invoke custom retry process.
     *
     * @param retryInfo
     * @return Object
     * @author Naughty Guo Jun 2, 2016
     */
    private Object invokeRetryProcess(RetryInfo retryInfo) {
        retryException(retryInfo);
        return retryResult(retryInfo);
    }

    /**
     * check whether do retry.
     *
     * @param retryInfo
     * @return boolean
     * @author Naughty Guo Jun 2, 2016
     */
    private boolean doRetry(RetryInfo retryInfo) {
        Method method = retryInfo.getTargetMethod();
        boolean enableRetry = method.isAnnotationPresent(EnableRetry.class);
        retryInfo.setEnableRetry(enableRetry);
        return enableRetry;
    }

    /**
     * init RetryInfo.
     *
     * @param retryInfo
     * @return RetryInfo
     * @author Naughty Guo Jun 2, 2016
     */
    private void fillRetryInfo(RetryInfo retryInfo) {
        Method method = retryInfo.getTargetMethod();
        EnableRetry enableRetry = method.getAnnotation(EnableRetry.class);
        retryInfo.fill(enableRetry);
    }

    /**
     * retry when throw exception.
     *
     * @param retryInfo
     * @return
     * @author Naughty Guo Jun 2, 2016
     */
    private void retryException(RetryInfo retryInfo) {
        try {
            retryInfo.setInvokeResult(invoke(retryInfo));
        } catch (Throwable e) {
            if (e instanceof RetryException && !retryInfo.isIgnoreException(e)) {
                if (retryInfo.isRetryException(e)) {
                    if (RetryCheck.doRetry(retryInfo)) {
                        try {
                            Thread.sleep(retryInfo.getInterval());
                        } catch (InterruptedException ie) {
                            LOGGER.warn("Wait method {} doing next retry error, it will do next retry immediately.",
                                    retryInfo.getTargetMethodName(), ie);
                        }
                        LOGGER.warn("Retry method {} error, it will retry {} times, current retry is {}.",
                                retryInfo.getTargetMethodName(), retryInfo.getTimes(), retryInfo.getExecutedTimes(), e);
                        /**
                         * keep the first invoke exception, which is the base retry check condition.
                         * whether do exception retry depend on the first exception.
                         */
                        if (null == retryInfo.getInvokeException()) {
                            retryInfo.setInvokeException(e);
                        }
                        retryException(retryInfo);
                    } else {
                        LOGGER.error("End retry still error, the method is {}", retryInfo.getTargetMethodName(),
                                retryInfo.getInvokeException());
                        if (StringUtils.isNotBlank(retryInfo.getFallbackMethod())
                                || retryInfo.getFallbackProvider() != FallbackProvider.class) {
                            retryInfo.setInvokeResult(FallbackUtil.fallback(retryInfo));
                        }
                        throw new RetryException("End retry still error.", retryInfo.getInvokeException());
                    }
                }
            }
            LOGGER.warn("Invoke method {} error.", retryInfo.getTargetMethod(), e);
            throw e;
        }
    }

    /**
     * retry when data not match rule.
     *
     * @param retryInfo
     * @return Object
     * @author Naughty Guo Jun 2, 2016
     */
    private Object retryResult(RetryInfo retryInfo) {
        if (StringUtils.isNotBlank(retryInfo.getExp()) && StringUtils.isNotBlank(retryInfo.getVal())) {
            boolean doRetry = false;
            if (null == retryInfo.getExpContext()) {
                Map<Object, Object> expContext = new HashMap<Object, Object>();
                expContext.put(retryInfo.getVal(), retryInfo.getInvokeResult());
                retryInfo.setExpContext(expContext);
            }
            try {
                doRetry = (boolean) MVEL.eval(retryInfo.getExp(), retryInfo.getExpContext());
                LOGGER.info("Invoke retry expression result is {}", doRetry);
            } catch (Exception e) {
                LOGGER.error("Invoke whether do retry expression error.", e);
                throw new RetryException("Invoke whether do retry expression error.", e);
            }
            if (doRetry) {
                if (RetryCheck.doRetry(retryInfo)) {
                    try {
                        Thread.sleep(retryInfo.getInterval());
                    } catch (InterruptedException ie) {
                        LOGGER.warn("Wait method {} doing next retry error, it will do next retry immediately.",
                                retryInfo.getTargetMethodName(), ie);
                    }
                    LOGGER.warn("Result is match retry expression, it will retry {} times, current retry is {}.",
                            retryInfo.getTimes(), retryInfo.getExecutedTimes());
                    retryException(retryInfo);
                    return retryResult(retryInfo);
                } else {
                    LOGGER.error("End retry still error, the method is {}", retryInfo.getTargetMethodName(),
                            retryInfo.getInvokeException());
                    if (StringUtils.isNotBlank(retryInfo.getFallbackMethod())
                            || retryInfo.getFallbackProvider() != FallbackProvider.class) {
                        return FallbackUtil.fallback(retryInfo);
                    }
                }
            }
        }
        return retryInfo.getInvokeResult();
    }
}
